SPB Git forge

spb/qc26

Public
10commits 1branches 0releases
2.8 MBsize
maindefault branch
17 days agolast push
Python 51.6% TypeScript 46.7% CSS 1.7%
3.6 KB · 50 lines tsx
Raw Blame History
1import type { Metadata } from "next";2import Link from "next/link";3import { notFound } from "next/navigation";4import { apiSafe, type Meta } from "@/lib/api";5import { PartyLogo } from "@/components/party";6import { Badge, Card, Empty } from "@/components/ui";7import { PARTY_IDS, partyVar, partyName } from "@/lib/parties";89export const revalidate = 300;1011interface Proposals { proposals: { id: number; party: string; topic: string; topicLabel: string; subtopic: string | null; proposal: string; position: string | null; direction: string | null; magnitude: string | null; page: number | null; quote: string; confidence: number; document: { id: number; title: string | null; url: string | null; type: string | null } }[] }1213export async function generateMetadata({ params }: { params: Promise<{ topic: string }> }): Promise<Metadata> {14  const { topic } = await params;15  const meta = await apiSafe<Meta>("/api/meta");16  const label = meta?.topics[topic];17  return { title: label ? `${label} — positions des partis` : "Enjeu", description: `Ce que proposent les partis sur ${label ?? "cet enjeu"} d'après leurs documents officiels, avec page et citation.` };18}1920export default async function Page({ params }: { params: Promise<{ topic: string }> }) {21  const { topic } = await params;22  const [meta, data] = await Promise.all([apiSafe<Meta>("/api/meta"), apiSafe<Proposals>(`/api/proposals?topic=${topic}&limit=400`)]);23  if (!meta || !meta.topics[topic]) notFound();24  if (!data) return <Empty title="Indisponible" />;25  const by: Record<string, Proposals["proposals"]> = {};26  data.proposals.forEach((p) => { (by[p.party] ??= []).push(p); });27  return (28    <div className="space-y-6">29      <div><Link href="/enjeux" className="text-[12.5px] text-ink-3 hover:text-ink">← Enjeux</Link><h1 className="display text-[36px] md:text-[48px] mt-2">{meta.topics[topic]}</h1><p className="text-ink-2 mt-2 text-[15px]">{data.proposals.length} propositions extraites des documents officiels. L&apos;absence d&apos;un parti signifie qu&apos;aucun document ingéré n&apos;aborde le sujet.</p></div>30      <div className="grid lg:grid-cols-5 gap-3">31        {PARTY_IDS.map((id) => (32          <div key={id} className="space-y-2">33            <div className="flex items-center gap-2 pb-2 border-b-2" style={{ borderColor: partyVar(id) }}><PartyLogo id={id} height={16} /><span className="font-semibold text-[13px]">{partyName(id)}</span><span className="num text-[12px] text-ink-3 ml-auto">{by[id]?.length ?? 0}</span></div>34            {(by[id] ?? []).length === 0 && <div className="text-[12.5px] text-ink-3 p-2">Aucune proposition documentée.</div>}35            {(by[id] ?? []).map((p) => (36              <Card key={p.id} className="!p-3 text-[12.5px] leading-snug" >37                <div className="font-medium">{p.proposal}</div>38                {p.magnitude && <div className="num text-ink-2 mt-1">{p.magnitude}</div>}39                <blockquote className="mt-2 italic text-ink-2 border-l-2 pl-2" style={{ borderColor: partyVar(id) }}>« {p.quote.length > 240 ? p.quote.slice(0, 240) + "…" : p.quote} »</blockquote>40                <div className="mt-2 flex items-center justify-between gap-2 text-[11.5px] text-ink-3"><Link href={`/documents/${p.document.id}#p${p.id}`} className="link truncate">{p.document.title ?? "Document"}</Link><span className="num shrink-0">{p.page ? `p. ${p.page} · ` : ""}{Math.round(p.confidence * 100)} %</span></div>41                {p.direction && p.direction !== "neutre" && <div className="mt-1.5"><Badge>{p.direction.replace(/_/g, " ")}</Badge></div>}42              </Card>43            ))}44          </div>45        ))}46      </div>47    </div>48  );49}50